package dk.au.cs.peterbn.processing.instanceJobs;

import dk.au.cs.peterbn.annotations.Pattern;
import dk.au.cs.peterbn.annotations.externals.ExternalRoleInformation;
import dk.au.cs.peterbn.annotations.externals.ExternalTypeInformation;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.processing.PatternInstance;
import dk.au.cs.peterbn.processing.PerInstanceProcessingJob;
import dk.au.cs.peterbn.processing.ProcessingUtil;

import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Checks that all type roles are present in a pattern instance.
 */
public class CheckAllTypeRolesPresent extends PerInstanceProcessingJob {
    private final RoundEnvironment env;
    private final ProcessingUtil util;

    public CheckAllTypeRolesPresent(RoundEnvironment env, Map<String, PatternInstance> patternInstances, ProcessingUtil util) {
        super(patternInstances);
        this.env = env;
        this.util = util;
    }

    @Override
    public void process(PatternInstance patternInstance) {
        //List of typeRoles that are required to be present in the pattern
        Set<TypeRole> typeRoles = new HashSet<TypeRole>(patternInstance.getName().getTypes());
        //Remove the roles present in the current compilation unit
        discoverCompilationUnitRoles(patternInstance, typeRoles);

        if (!typeRoles.isEmpty()) {
            //Remove the roles present on the build path.
            discoverClassPathRoles(patternInstance, typeRoles);
        }

        if (!typeRoles.isEmpty()) {
            //Finally, see if we have any external class information in xml resources
            discoverExternalRoles(patternInstance, typeRoles);
        }

        //Complain if we are missing some elements
        if (!typeRoles.isEmpty()) {
            String msg = "Incomplete design pattern instance: " + patternInstance.getId() + ". Missing elements: {";
            for (TypeRole typeRole : typeRoles) {
                msg += " " + typeRole.getName();
            }
            msg += "}";
            for (Element element : patternInstance.getElements()) {
                util.printMessage(Diagnostic.Kind.ERROR, msg, element);
            }
        }
    }

    /**
     * Filters out all the roles that are present in the current compilation unit, leaving back the roles that should be located on the classpath.
     * @param patternInstance The working instance.
     * @param typeRoles The set of TypeRoles to filter
     */
    private void discoverCompilationUnitRoles(PatternInstance patternInstance, Set<TypeRole> typeRoles) {
        for (TypeElement element : ElementFilter.typesIn(patternInstance.getElements())) {
            List<AnnotationMirror> mirrors = util.getPatternAnnotationMirrors(element);
            for (AnnotationMirror mirror : mirrors) {
                TypeRole typeRole = util.getTypeRole(element, mirror);
                String id = util.getId(mirror);
                if (patternInstance.getId().equals(id)) {
                    typeRoles.remove(typeRole);
                    if (typeRole != null
                            && (typeRole.getTarget() == TypeRole.Target.ABSTRACT || (typeRole.getTarget() == TypeRole.Target.ANY ))
                            && util.getValue(mirror, Pattern.Fields.OMIT_IMPLEMENTATION).length() > 0) {
                        typeRoles.removeAll(typeRole.getImplementers());
                    }
                }
            }
        }
    }

    /**
     * Filters out all the roles that can be found on the ClassPath by looking for annotated classes. This leaves behind the roles
     * that should be discovered by ExternalTypeInformation
     * @param patternInstance The current pattern instance
     * @param typeRoles The set of TypeRoles to filter.
     */
    private void discoverClassPathRoles(PatternInstance patternInstance, Set<TypeRole> typeRoles) {
        Set<Class<?>> withPatterns = util.getClassPathTypesWithPatterns();
        Set<Pattern> patternAnnotations = new HashSet<Pattern>();
        //Collect the pattern annotations
        classLoop:
        for (Class<?> withPattern : withPatterns) {
            //Don't load classes we are compiling.
            //todo: This could probably be optimized with a datastructure lookup
            for (Element element : env.getRootElements()) {
                String elementTypeName = element.asType().toString();
                String loadedTypeName = withPattern.getCanonicalName();
                //Using startsWith to catch inner classes
                if (loadedTypeName.startsWith(elementTypeName)) {
                    continue classLoop;
                }
            }

            patternAnnotations.addAll( util.getAllPatternAnnotations(withPattern));
        }
        //Get the corresponding typeRoles
        for (Pattern patternAnnotation : patternAnnotations) {
            if (patternAnnotation.id().equals(patternInstance.getId())) {
                String role = patternAnnotation.role();
                TypeRole typeRole = patternInstance.getName().getTypeRole(role);
                if (typeRole != null) {
                    typeRoles.remove(typeRole);
                }
            }
        }
    }

    private void discoverExternalRoles(PatternInstance patternInstance, Set<TypeRole> typeRoles) {
        Set<ExternalTypeInformation> externalTypeInformations = util.getExternalInformation(patternInstance.getId());
        for (ExternalTypeInformation externalTypeInformation : externalTypeInformations) {
            Set<ExternalRoleInformation> rolesInformation = externalTypeInformation.getRolesInformation();
            for (ExternalRoleInformation externalRoleInformation : rolesInformation) {
                TypeRole typeRole = externalRoleInformation.getTypeRole();
                typeRoles.remove(typeRole);
            }
        }
    }


}
